#!/usr/bin/env ruby
# The Logger class is automatically instantiated when RedBook is started.
# It is responsible of logging, retrieving, updating, exporting and deleting messages
# inside RedBook log files.
class RedBook::Logger

	include Observable
	include RedBook::Utils

	attr_reader :tags, :variables, :macros, :alias
	attr_writer :interface_parser
	attr_writer :interface_input

	@@config = {}

	# Initialize all the class and instance variables.
	def initialize
		@hooks = RedBook::HookCollection.new
		@command, @renderers = {}, {}
		@log, @tags, @macros, @variables, @dataset, @output, @previous_output = [], [], [], [], [], [], []
		@alias = ''
	end
	
	def current_log
		@log
	end
	
	def current_dataset
		@dataset
	end

	# Start RedBook Logger, initialize parser, plugins, renderers, load configuration file and default log.
	def start
		notify({:separator => nil})
		notify({:generic => " #{RedBook::WELCOME}"})
		notify({:separator => nil})
		@parser = RedBook::Parser.new
		# Initialize Plugins
		RedBook::LOADED_PLUGINS.each do |p|
			send :"initialize_#{p}" if respond_to?(:"initialize_#{p}")
		end
		verify_loaded_objects(RedBook::PLUGINS, RedBook::LOADED_PLUGINS, 'plugin')
		# Initialize Renderers
		RedBook::LOADED_RENDERERS.each do |r|
			@renderers[r] = instance_eval "RedBook::#{r.camelcase}Renderer.new" unless r == 'console'
		end
		verify_loaded_objects(RedBook::RENDERERS, RedBook::LOADED_RENDERERS, 'renderer')
		# Load configuration file
		process ":config :silent"
		reset
		unless @@config[:data] then
			notify({:error => "Configuration file not found."})
			exit
		end
		@@config[:data_default] = @@config[:data]
		@alias = 'default'
		# Load the default log into memory
		if RedBook::MODE == 'gem' && (@@config[:data] == 'log.yml' || !File.exist?(@@config[:data].to_s))
			# Create log if necessary
			File.open("#{ENV['HOME']}/log.yml", 'w+') unless File.exist?("#{ENV['HOME']}/log.yml")
			process ":set data :value #{@parser.escape(ENV['HOME'])}/log.yml :silent"
		end
		unless File.exists?(@@config[:data])
			notify({:error => "Logger Error: Default log file not found."})
			process ':quit'
		end
		process ":refresh :silent"
		### HOOK
		@hooks.exec :after_start
	end

	def reset
		@output = []
		@command = {}
		@log_frame = @log
	end

	# Parse user command and execute the specified operation
	def process(input, use_interface_parser=false)
		reset
		begin
			# Populate the @command hash
			@command = (use_interface_parser) ? @interface_parser.parse(input) : @parser.parse(input)
			if RedBook::Parser.operation? @command[:operation]
				notify(@command.to_xhash_command)
				# Call the specified operation, if it is valid
				send(@command[:operation])
			else
				notify({:error => "Operation invalid or not specified."})
			end
		rescue Exception => e
			# Rescue exceptions and exits
			if e.is_a? SystemExit then
				exit
			else
				bt = []
				e.backtrace.each { |l|	bt << {:debug => l} } if (@command[:debug] || RedBook::DEBUG)
				notify({:error => "#{e.class.to_s.underscore.titleize}: #{e.to_s}", :_children => bt})
			end
		end
		@previous_output = @output.compact
	end

	# Exit RedBook Logger
	def quit
		notify({:info => "Exiting..."})
		exit
	end

	# Query a log file and populate a dataset, displaying results.
	#
	# <i>Message Format:</i> regular expression (to match the activity message)
	# 
	# <i>Additional Directives</i>
	# * <tt>:with|:tags</tt> - Allows to specify tag-based filters (limited boolean logic accepted)
	# * <tt>:avg</tt> - Calculates the average time spent <i>(Tracker Plugin)</i>
  # * <tt>:total</tt> - Calculates the total time spent <i>(Tracker Plugin)</i>
  # * <tt>:count</tt> - Calculates the number of messages returned <i>(Tracker Plugin)</i>
  # * <tt>:duration</tt> - Calculates the duration of each completed activity <i>(Tracker Plugin)</i>
  # * <tt>:from|:since</tt> - Restrict the activities returned to only those after a specified time expression
  # * <tt>:to|:till|:until</tt> - Restrict the activities returned to only those before a specified time expression
  # * <tt>:silent</tt> - No output will be displayed to the user
  # * <tt>:first|:last</tt> -  Restrict the activities returned to the first/last _N_
  # * <tt>:pending|:completed|:plain</tt> - Restrict the activities returned by status
	def select
		# Determine the limits and empty the dataset
		limit = @command[:first] || @command[:last] || @log.length
		raise LoggerError, "Start time is earlier than end time."  if @command[:from] && @command[:to] && @command[:to] < @command[:from]
		lower_time_limit = (@command[:from]) ? find_time_limit(@command[:from], :from) : 0
		upper_time_limit = (@command[:to]) ? find_time_limit(@command[:to], :to) : @log.length-1
		@log_frame = @log[lower_time_limit..upper_time_limit]
		@dataset = []
		count = 0
		notify({:debug => "Messages in Log: #{@log.length}"})
		notify({:debug => "Messages in Log Frame: #{@log_frame.length}"})
		# Override activity status modifiers in case of an aggregation
		if (@command[:duration] || @command[:avg] || @command[:total]) then
				@command[:plain] = nil
				@command[:completed] = nil
				@command[:pending] = nil
		end
		# Process the log and populate the dataset if activities match
		if @command[:first]
			@log_frame.each do |m|
				break if count >= limit
				count+=1 if match(m) 
			end
		else
			# Do the same, but reverse the log order
			@log_frame.reverse.each do |m|
				break if count >= limit
				count+=1 if match(m)
			end
			# Reverse the dataset order (it must be always the same)
			@dataset = @dataset.reverse
		end
		notify({:debug => "Messages in Dataset: #{@dataset.length}"})
		unless @dataset.empty? then
			### HOOK
			unless @hooks.exec(:select_flag, {:command => @command}) then
				count = 0
				# Display activities
				@dataset.each do |m|
					count+=1
					notify(m.to_xhash_activity, {:index => count, :total => @dataset.length})
				end
				obj = (@dataset.length == 1) ? 'activity' : 'activities'
				notify({:success => "#{@dataset.length} #{obj} loaded into current dataset."})
			end
		else
			notify({:warning => 'No activities found matching the criteria you specified.'})
		end
	end

	# Calculate the time elapsed between two given activities (requires a dataset).
	#
	# <i>Message Format:</i> activity index/indices (space-separated)
	def calc
		count = 0
		previous = nil
		unless @dataset.empty? then
			@dataset.each do |m|
				count+=1
				if !@command[:completed] && (@command[:selected] == true || (@command[:selected].include? count)) then
					notify({:duration => m[:timestamp]-previous}) if previous
					notify(m.to_xhash_activity, {:index => count, :total => @dataset.length})
					previous = m[:timestamp]
				end
			end
				notify({:duration => Time.now-previous})
				notify({:timestamp => Time.now, :message => "[CURRENT TIME]"}.to_xhash_activity, {:index => '-', :total => @dataset.length})
		else
			notify({:warning => "Empty Dataset."})
		end
	end

	# Log an activity.
	#
	# <i>Message Format:</i> the activity to log.
	#
	# <i>Additional Directives</i>
	# * <tt>:dest</tt> - a log alias where the message will be saved (assumed to be "default")
	# * <tt>:with|:tags</tt> - a list of space-separated tags
	def log
		if @command[:message]
			raise LoggerError, "You cannot log a completed activity" if @command[:message].match(/^\[COMPLETED\]/)
			# Switch log if :dest is specified
			previous_log = switch_log(@command[:dest]) if @command[:dest]
			message = {:timestamp => Time.now, :message => @command[:message]}
			message[:tags] = @command[:tags] if @command[:tags]
			unless @command[:dest] then
				# Unless :dest is specified, add the new activity to the current log and dataset
				@log << message 
				@dataset << message
				if message[:tags] then
					message[:tags].each do |t|
						@tags << t unless @tags.include? t
					end
				end
			end
			begin
				# Writes the activity to the log file
				log_message(message, @@config[:data])
				switch_log(previous_log) if @command[:dest]
			rescue
				#raise LoggerError, "Unable to save log message."
			end
		else
			notify({:warning => "Nothing to log..."})
		end
	end

	# Complete an uncompleted activity by relogging it with a current timestamp
	# and prepending [COMPLETE] to it.
	#
	# <i>Message Format:</i> the activity index to complete
	def finish
		# If no message was specified, complete either:
		# * the last activity in the current dataset
		# * the last activity in the current log (if the dataset is empty)
		if @command[:message] == "" then
			if @dataset.length > 0 then
				m = @dataset[@dataset.length-1].dup 
			else
				m = @log[@log.length-1].dup
			end
		elsif @dataset[@command[:message].to_i-1] then 
			m = @dataset[@command[:message].to_i-1].dup 
		else
			raise LoggerError, "Message index not found in current dataset."
		end
		# Prevent completing an already-completed activity
		raise LoggerError, "Activity already completed" if m[:message].match(/^\[COMPLETED\]/)
		# Modify the message and the timestamp
		m[:message] = "[COMPLETED] "+m[:message]
		m[:timestamp] = Time.now
		# Log the completed activity
		log_message(m, @@config[:data])
		@log << m
		@dataset << m
	end

	# Re-log a previously-logged, not-completed activity (requires a dataset).
	#
	# <i>Message Format:</i> the index of the activity to re-log
	def relog
		# Duplicate the logged activity
		if @dataset[@command[:message].to_i-1] then 
			m = @dataset[@command[:message].to_i-1].dup 
		else
			raise LoggerError, "Message index not found in current dataset."
		end
		raise LoggerError, "You cannot re-log a completed activity" if m[:message].match(/^\[COMPLETED\]/)
		# Change the timestamp
		m[:timestamp] = Time.now
		# re-log the activity
		log_message(m, @@config[:data])
		@log << m
		@dataset << m
	end

	# Update apreviously-logged activity
	#
	# <i>Message Format:</i> The index of the activity to update
	#
	# <i>Additional Directives</i>
	# * <tt>:with</tt> - the list of tags to add or remove
	# * <tt>:message</tt> - the message to update.
	def update
		# Select the message to update
		if @dataset[@command[:message].to_i-1] then 
			m = @dataset[@command[:message].to_i-1]
			changed = false
		else
			raise LoggerError, "Message index not found in current dataset."
		end
		if !@command[:message] && !@command[:tags] then
			notify({:warning => "Nothing to modify."})
		else
			### HOOK
		  @hooks.exec(:before_update, {:activity => m})
		  notify({:info => "Updating message..."})
			# Update message and tags
			if @command[:text]	
				m[:message] = @command[:text]
				changed  = true
			end
			if @command[:tags] then
				m[:tags] ||= []
				@command[:tags].each do |t|
					tag = t.gsub(/^[-+]/, '')
					case t
					when /^-/ then
						if m[:tags].include? tag then
							m[:tags].delete(tag)
							changed = true
						else
							notify({:warning => "Message is not tagged with '#{tag}'."})
						end
					when /^\+/ then
						if m[:tags].include? tag then
							notify({:warning => "Message is already tagged with '#{tag}'."})
						else
							m[:tags] << tag
							changed = true
						end
					else
						notify({:warning => "Please specify whether to add (+) or remove (-) tag '#{t}'."})
					end
				end
			end
			# Overwrite the whole log file
			update_message = (changed) ? "Message updated." : "Nothing to update."
			overwrite_log if changed
			reset
			notify({:info => update_message})
			### HOOK
			@hooks.exec(:after_update, {:activity => m})
		end
	end

	# Delete one or more previously-logged activities (requires a dataset).
	#
	# <i>Message Format:</i> the index/indices of the activities to delete (or none to delete the whole dataset)
	def delete
		count = 0
		# If no indices are specified (@command[:selected] == true), select the whole dataset
		if @command[:selected] == true then
			@command[:selected] = []
			@dataset.each_index do |i|
				@command[:selected] << i+1
			end
		end
		target = (@command[:selected].length == 1) ? "this message" : "these messages"
		s = (@command[:selected].length == 1) ? "" : "s"
		unless @dataset.empty? then
			# Print activities to delete
			unless @command[:silent] then
				notify({:warning => "Preparing to delete the following message#{s}:"})
				notify({:separator => nil})
				@dataset.each do |m|
					count+=1
					if @command[:selected].include? count then
						notify(m.to_xhash_activity, {:index => count, :total => @dataset.length})
					end
				end
				notify({:separator => nil})
				answer = user_confirmation("Do you really want to delete #{target}?")
			else
				answer = true
			end
			not_found = 0
			if answer then
				### HOOK
				@hooks.exec(:before_delete, {:activities => @command[:selected]})
				notify({:info => "Deleting #{(@command[:selected].length == 1) ? 'message' : 'messages'}..."})
				# Delete activities
				@command[:selected].each do |i|
					index = find_message(@dataset[i-1])
					if index then
						@log.delete_at(index)
					else
						notify({:warning => "Message [#{i}] not found."})
						not_found += 1
					end
				end
				@dataset = []
				selected = @command[:selected]
				# Overwrite the whole log file
				overwrite_log
				reset
				notify({:success => "#{selected.length - not_found} message#{s} deleted."})
			else
				notify({:info => "Operation aborted."})
			end
			### HOOK
			@hooks.exec(:after_delete, {:activities => @command[:selected]})
		else
			notify({:warning => "Empty Dataset."})
		end
	end


	# Switch to another log file
	#
	# <i>Message Format:</i> a valid log alias
	def use
		### HOOK
		@hooks.exec(:before_use, {:log => @command[:message]})
		@log = []
		switch_log(@command[:message]) unless @command[:message].blank?
		begin
			notify({:info => "Loading #{@alias} log file..."})
			data = load_yaml_file(Array, @@config[:data])
		rescue
			if user_confirmation("The log file \"#{@@config[:data]}\" does not exist. Do you wish to create it?") then
				begin 
				create_file(@@config[:data])
				notify({:success =>"#{File.expand_path(file)} created."})
				if user_confirmation("Do you want to save the absolute path of this log to your configuration file?") then
					al = user_input("Please enter the full alias for this log (data, data_test, ...)", "alias")
					process(":set #{al} :value #{File.expand_path(file)}") if al
				end
				rescue
					raise LoggerError, "Unable to create log file."
				end
			else
				notify({:warning => "Operation aborted."})
			end
		end
		# process messages
		data.each do |e| 
			if (e.is_a? Hash) && (!e.is_a? Array) then
				@log << e
			else
				e.each do |m|
					@log << m
				end
			end
		end
		notify({:warning => "#{@alias.capitalize} is empty."}) if @log.blank?
		notify({:success => "#{@alias.capitalize} log loaded."})
		# Load the log's tag into memory for auto-completion
		collect_tags
		reset
		@dataset = []
		### HOOK
		@hooks.exec(:after_use, {:log => @command[:message]})
	end

	# Reload the current log into memory
	# <i>Message Format:</i> n/a
	def refresh
		@command[:message] = ''
		use
	end

	# (Re)load the configuration file into memory and print it to screen.
	#
	# <i>Message Format:</i> n/a
	def config
		@@config = RedBook::Config.instance.load
		collect_variables
		collect_macros
		notify({:separator => nil})
		cfg = sort_config
		cfg.each_pair do |l, v|
			notify(v)
		end
		notify({:separator => nil})
	end

	# Add/edit/delete a configuration setting
	#
	# <i>Message Format:</i> the setting to access
	#
	# <i>Additional Directives</i>
	# * <tt>:value</tt> - the new value for the specified setting
	def set
		### HOOK
		@hooks.exec(:before_set, {:setting => @command[:message], :value => @command[:value] })
		raise LoggerError, "Setting not specified." if @command[:message].blank?
		if @command[:value].blank? then
			if user_confirmation("Do you really want to delete :#{@command[:message]}?") || @command[:silent] then
				@@config.delete(@command[:message].to_sym)
				save_config
				@command[:silent] = true
				config
			else
				notify({:warning => "Operation aborted."})
			end
		elsif user_confirmation("Do you want to set [#{@command[:message]}] to [#{@command[:value]}]?") || @command[:silent] then
			@command[:value].sub!(/^["']+/, '')
			@command[:value].sub!(/["']+$/, '')
			# Identify value type...
			value = nil
			case @command[:value]
			when /^true|false$/i then
				value = @command[:value].match(/true/i) ? true : false
			when /^d+$/ then
				value = @command[:value].to_i
			when /^[-+]?\d*\.?\d*$/
				value = @command[:value].to_s
			else
				value = @command[:value]
			end
			@@config[@command[:message].to_sym] = value
			save_config
			process ":config :silent"
		end
		### HOOK
		@hooks.exec(:after_set, {:setting => @command[:message], :value => @command[:value] })	
	end

	# Clear the screen.
	#
	# <i>Message Format:</i> n/a
	def clear
		PLATFORM.match(/win32/) ? system('cls') : system('clear')
	end

	# Evaluate a valid Ruby expression (via <tt>instance_eval</tt>).
	#
	# <i>Message Format:</i> Ruby code to be evaluated
	def ruby
		res = instance_eval(@command[:message]).to_s
		if res.is_a? Array then
			res.each {|e| notify({:generic => e}) }
		else
			notify({:generic => res })
		end
	end


	# Dump the activities in the current dataset.
	#
	# <i>Message Format:</i> [file name] Dump the current dataset to a [file name] YAML file, if supplied
	def dataset
		### HOOK
		@hooks.exec(:before_dataset, {:file => @command[:message]})
		count = 0
		if @command[:message] then
			notify({:info => "Dumping dataset..."})
			@command[:message]||=@@config[:default_save_file]
			raise LoggerError, "No file specified." if @command[:message].blank?
			lines = []
			@dataset.each {|a| lines << YAML::dump(a).encode_out }
			write_file(lines, @command[:message])
			notify({:success => "Dataset dumped successfully"})
		else
			@dataset.each {|a| count +=1; notify( a.to_xhash_activity, {:index => count, :total => @dataset.length }) }
		end
		### HOOK
		@hooks.exec(:after_dataset, {:file => @command[:message]})
	end

	# Save the current dataset to an external file 
	# (the format depends on the loaded renderers).
	#
	# <i>Message Format:</i> [file name]
	#
	# <i>Additional Directives</i>
	# * <tt>:type</tt> - Override the default save file type
	def save
		notify({:info => "Saving output..."})
		@command[:message]||=@@config[:default_save_file]
		@command[:type]||=@@config[:default_save_file_type]
		raise LoggerError, "#{@command[:type].capitalize} Renderer not found." unless @renderers[@command[:type]]
		raise LoggerError, "No file specified." unless @command[:message]
		renderer = @renderers[@command[:type]]
		lines = []
		notify({:debug => "Total Output Messages: #{@previous_output.length}"})
		out = {}
		if @previous_output[0] then
			# The first element of the output is always a command
			out = @previous_output[0][:element]
			@previous_output[1..@previous_output.length].each do |v|
				out << v[:element]
			end
		end
		renderer.render(out)
		unless renderer.output.blank?
			### HOOK
			write_file(renderer.output, @command[:message]) unless @hooks.exec(:save_file, {:output => renderer.output, :file => @command[:message], :type => @command[:type]})
			notify({:success => "Output saved successfully"})
		else
			notify({:warning => "Nothing to save..."})
		end
		renderer.reset
	end

	private

	def sort_config
		scfg = {}
		### HOOK
		@hooks.exec :sort_config_header, {:scfg => scfg}
		scfg[:macros] = {:info => 'Macros'}
		scfg[:variables] = {:info => 'Variables'}
		scfg[:log_aliases] = {:info => 'Log Aliases'}
		scfg[:saving_settings] = {:info => 'Saving Settings'}
		scfg[:general_settings] = {:info => 'General Settings'}
		scfg[:charset_settings] = {:info => 'Character Set Settings'}
		@@config.each_pair do |n, v|
			### HOOK
			unless (@hooks.exec(:sort_config_match, {:scfg => scfg, :index => n, :value => v})) then
				case n.to_s
				when /^m_/
					scfg[:macros] << {n => v, :_attributes => {:raw => true}}
				when /^var_/
					scfg[:variables] << {n => v, :_attributes => {:raw => true}}
				when /^default_save_/
					scfg[:saving_settings] << {n => v, :_attributes => {:raw => true}}
				when /charset$/
					scfg[:charset_settings] << {n => v, :_attributes => {:raw => true}}
				when /^data/
					scfg[:log_aliases] << {n => v, :_attributes => {:raw => true}} unless n == :data_default
				else
					scfg[:general_settings] << {n => v, :_attributes => {:raw => true}}
				end
			end
		end
		scfg
	end

	def save_config
		old_config = @@config
		contents = []
		cfg = sort_config
		cfg.each_pair do |l, v|
			contents << "## #{v.value}" # header
			v.children.each do |c|
			value = c.value.is_a?(String) ? "\"#{c.value}\"" : c.value
				contents << ":#{c.name}: #{value}" # setting
			end
			contents << '' # empty line
		end
		begin
			write_file(contents, RedBook::CONFIG)
			@@config = RedBook::Config.instance.load
		rescue
			@@config = old_config
			raise LoggerError, "Unable to save configuration file."
		end
		notify({:success => "Configuration Saved."})
	end

	def switch_log(log_alias)
		unless log_alias.nil? then
			notify({:info => "Switching to #{log_alias} log..."})
			if @@config[:"data_#{log_alias}"] then
				previous_alias = @alias
				@alias = log_alias
				@@config[:data] = @@config[:"data_#{log_alias}"]
				return previous_alias
			else
				raise LoggerError, "Log alias \"#{log_alias}\" not defined."
			end
		else
			return @alias
		end
	end

	def get_log_path(log_alias)
		begin
			return (log_alias == "default") ? @@config[:data] : @@config[:"data_#{log_alias}"]
		rescue
			raise LoggerError, "Log alias \"#{log_alias}\" not defined."
		end
	end

	def collect_tags
		@tags = []
		@log.each do |m|
			@tags.concat(m[:tags]) if m[:tags]
		end
		@tags.merge!(RedBook::Parser.special_tags)
	end

	def user_confirmation(sentence)
		@command[:silent] ? true : user_input(sentence, 'data', /^(Yes|y)$/i, /^(No|n)$/i)
	end

	def user_input(sentence, object="data", regex_positive=/.*/, regex_negative=nil)
		@interface_input.call sentence, object, regex_positive, regex_negative
	end

	def log_message(activity, file)
		### HOOK
		@hooks.exec :before_log_message, {:activity => activity, :file => file}
		write_file(activity.to_yaml, file, 'a')
		notify({:success => "Logged."})
		### HOOK
		@hooks.exec :after_log_message, {:activity => activity, :file => file}
	end

	def find_message(m)
		@log_frame.each_index do |i|
			return i if @log_frame[i] == m
		end
		return false
	end

	def find_by_timestamp(ts)
		@log_frame.each do |m|
			return m if m[:timestamp] == ts
		end
		return false
	end

	def completed?(m)
		return (m[:message].match(/\[COMPLETED\] /)) ? true : false
	end
	def concurrent?(m)
		if m[:tags] then
			if m[:tags].include?('_concurrent') then
				return true
			end
		end
		false
	end

	def break?(m)
		if m[:tags] then
			if m[:tags].include?('_break') then
				return true
			end
		end
		false
	end
	def relogged_message?(m1, m2)
		_m1 = m1.dup
		_m2 = m2.dup
		_m1[:timestamp] = _m2[:timestamp] = nil
		return (_m1 == _m2) ? true : false
	end


	def collect_variables()
		collect_config_entities(@variables, /^var_[a-z_]+/, /^var_/, ':%')
	end

	def collect_macros()
		collect_config_entities(@macros, /^m_[a-z_]+/, /^m_/, ':')
	end

	def collect_config_entities(dest, regexp, orig, repl)
		@@config.each_pair do |i, v|
			if i.to_s.match(regexp)
				dest << i.to_s.gsub(orig, repl)
			end
		end
	end
	def overwrite_log
		process ":select :silent"
		process ":dataset #{@@config[:data]} :silent"
	end

	def match(m)
			if match_pending(m) then
				if match_completed(m) then
					if match_plain(m) then
						if match_message(m) then
							if match_tags(m) then
								### HOOK
								unless @hooks.exec(:match_activity, {:activity => m}) then
									@dataset << m
								end
								return true
							end
						end
					end
				end
			end
		return false
	end

	def match_message(m)
		if @command[:message] && @command[:message] != "" then
			begin
				return @command[:negate] ? !m[:message].match(instance_eval(@command[:message])) : m[:message].match(instance_eval(@command[:message]))
			rescue Exception => e
				raise LoggerError, "Please provide a valid regular expression, e.g. /Test/ or /test/i"
			end
		else
			true
		end
	end

	def match_tags(m)
		if @command[:tags] then
			return false if m[:tags].blank?
			alt_result = []
			@command[:tags].each do |alt| # There may be more than one tag sets
				check = true
				alt[:positive].each { |t| check = false unless m[:tags].include? t }
				alt[:negative].each { |t| check = false if m[:tags].include? t }
				alt_result << check
			end
			(alt_result.include? true) ? true : false	
		else
			true
		end
	end

	def match_plain(m)
		unless @command[:plain]
			return true
		end
		if !m[:message].match(/\[COMPLETED\] /) then
			return true
		else
			return false
		end
	end

	def match_completed(m)
		unless @command[:completed] then
			return true
		end
		if m[:message].match(/\[COMPLETED\] /) then
			return true
		else
			return false
		end
	end

	def match_pending(m)
		unless @command[:pending]
			return true
		end
		@command[:plain] = true
		if match_plain(m) then
			completed = false
			@log_frame.each do |ms|
				if matching_completed_activity?(m, @log_frame[find_message(ms)])
					completed = true
				end
			end
			return  (completed) ? false : true
		else
			return false
		end
	end
	
	def matching_completed_activity?(start_m, end_m)
		if end_m[:message] == "[COMPLETED] #{start_m[:message]}" && start_m[:timestamp] < end_m[:timestamp] then
			if start_m[:tags] then
				if start_m[:tags] == end_m[:tags] then
					true
				else
					false
				end
			else
				true
			end
		end
	end	
	
	def find_time_limit(timestamp, mode, min=0, max=@log.length-1)
		i = ((min+max)/2).ceil
		t =	@log[i][:timestamp]
		return ((mode == :from) ? max : min) if max-min <= 1
		(timestamp >= t) ? find_time_limit(timestamp, mode, i, max) : find_time_limit(timestamp, mode, min, i)
	end


	def verify_loaded_objects(total, effective, object="file")
		difference  = total - effective
		if difference.length > 0 then
			warning = {:warning => "The following #{object.pluralize} could not be loaded:"}
			difference.each do |o|
				warning << {:"#{object}" => o.titleize}
			end
			notify(warning)
		end
	end

	def notify(element, options={})
		### HOOK
		@hooks.exec(:before_notify, {:element => element, :options => options})
		changed
		unless (element.name == :debug && !RedBook::DEBUG && !@command[:debug]) then
			@output << {:element => element, :options => options}
			notify_observers(element, options) unless @command[:silent] && element.name != :error
		end
		### HOOK
		@hooks.exec(:after_notify, {:element => element, :options => options})
	end	

end
